package p.ithorns.framework.logger.stream;


import ch.qos.logback.core.Context;
import ch.qos.logback.core.recovery.RecoveryCoordinator;
import ch.qos.logback.core.status.ErrorStatus;
import ch.qos.logback.core.status.InfoStatus;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.StatusManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import p.ithorns.framework.logger.ThreadPoolFactory;

import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;


/**
 * 基于PipeWriter实现多终端日志输出流
 *
 * 参考并复制了{@link ch.qos.logback.core.recovery.ResilientOutputStreamBase}的部分代码，
 * 来实现错误和异常状态处理
 *
 * @author 2021/10/23 20:35 by lht
 */
public class PipeLogOutputStream extends OutputStream {

    private final static Logger log = LoggerFactory.getLogger(PipeLogOutputStream.class);

    private final static int STATUS_COUNT_LIMIT = 2 * 4;

    private int statusCount = 0;

    private int noContextWarning = 0;

    private boolean presumedClean = true;

    private RecoveryCoordinator recoveryCoordinator;

    private Context context;

    private ExecutorService executor;

    private final Collection<PipedWriter> writers;

    public PipeLogOutputStream() {
        this.writers = new CopyOnWriteArraySet<>();
        this.executor = ThreadPoolFactory.createDefaultPool("web-roll-log");
    }

    /**
     * 使用PipeWriter实现write(byte[] bs)方法
     * 将byte[]转换成String由PipeWriter输出
     *
     * @param bs -字节数据
     */
    @Override
    public void write(@NotNull byte[] bs) {
        if (isPresumedInError()) {
            if (!recoveryCoordinator.isTooSoon()) {
                attemptRecovery();
            }

            // return regardless of the success of the recovery attempt
            return;
        }

        for (PipedWriter writer : writers) {
            executor.execute(() -> {
                try {
                    String content = new String(bs, StandardCharsets.UTF_8);
                    writer.write(content);
                    postSuccessfulWrite();
                } catch (IOException e) {
                    postIOFailure(e);
                }
            });
        }
    }


    @Override
    public void flush() {
        for (Writer writer : writers) {
            executor.execute(() -> {
                try {
                    writer.flush();
                    postSuccessfulWrite();
                } catch (IOException e) {
                    postIOFailure(e);
                }
            });
        }
    }


    @Override
    public void write(int b) {
        throw new UnsupportedOperationException();
    }


    @Override
    public void close() throws IOException {
        for (PipedWriter writer : writers) {
            writer.close();
        }

        executor.shutdown();
    }

    public void addWriter(PipedWriter writer) {
        this.writers.add(writer);
    }

    public void removeWriter(PipedWriter writer) {
        this.writers.remove(writer);
    }


    public void setContext(Context context) {
        this.context = context;
    }


    private void postSuccessfulWrite() {
        if (recoveryCoordinator != null) {
            recoveryCoordinator = null;
            statusCount = 0;
            addStatus(new InfoStatus("Recovered from IO failure", this));
        }
    }

    private void postIOFailure(IOException e) {
        addStatusIfCountNotOverLimit(new ErrorStatus("IO failure while writing", this, e));
        presumedClean = false;
        if (recoveryCoordinator == null) {
            recoveryCoordinator = new RecoveryCoordinator();
        }
    }

    private boolean isPresumedInError() {
        return (recoveryCoordinator != null && !presumedClean);
    }


    private void addStatusIfCountNotOverLimit(Status s) {
        ++statusCount;
        if (statusCount < STATUS_COUNT_LIMIT) {
            addStatus(s);
        }

        if (statusCount == STATUS_COUNT_LIMIT) {
            addStatus(s);
            addStatus(new InfoStatus("Will supress future messages regarding", this));
        }
    }

    private void addStatus(Status status) {
        if (context == null) {
            if (noContextWarning++ == 0) {
                System.out.println("LOGBACK: No context given for " + this);
            }
            return;
        }
        StatusManager sm = context.getStatusManager();
        if (sm != null) {
            sm.add(status);
        }
    }

    private void attemptRecovery() {
        try {
            super.close();
        } catch (IOException e) {
            log.error(e.getMessage());
        }

        addStatusIfCountNotOverLimit(new InfoStatus("Attempting to recover from IO failure。", this));

        // return regardless of the success of the recovery attempt
        this.presumedClean = true;
    }

}